package com.bing;

import com.bing.entity.Account;
import com.bing.entity.Answer;
import com.bing.entity.Note;
import com.bing.entity.Question;
import com.bing.entity.Review;
import com.bing.entity.Sex;
import com.bing.entity.Status;
import com.bing.exception.MFWException;
import com.google.gson.Gson;
import org.jsoup.Connection;
import org.jsoup.HttpStatusException;
import org.jsoup.Jsoup;
import org.jsoup.helper.StringUtil;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.SocketTimeoutException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Created by kaibb on 15-10-20.
 */
public abstract class Utils {

    private static final Logger LOGGER = LoggerFactory.getLogger(Utils.class);
    private static final Gson gson = new Gson();
    public static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    public static final SimpleDateFormat QUESTION_TIME = new SimpleDateFormat("yy/MM/dd HH:mm");
    public static final SimpleDateFormat ANSWER_TIME = new SimpleDateFormat("yyyy-MM-dd HH:mm");

    /**
     * 查找数字正则表达式
     */
    private static final Pattern PATERN = Pattern.compile("\\((?<num>\\d{1,})\\)");

    /**
     * 抽取问答ID,如下:4509016
     * /wenda/detail-4509016.html
     */
    private static final Pattern WENDA_ID = Pattern.compile("detail-(?<num>\\d{1,}).html");

    private static final Pattern NUMWITHPATTERN = Pattern.compile("(?<num>\\d{1,})");
    private static final Pattern UID_PATTERN = Pattern.compile("/(?<uid>\\d{1,}).html");
    public static final String REFERRER = "http://www.mafengwo.cn/wenda/";
    public static final int PAGE_SIZE = 5000000;
    public static final int ERROR = 500;
    public static final int NOT_FOUND = 404;

    /**
     * 请求页面cookies
     */
    private static Map<String, String> cookies = new HashMap<String, String>();

    /**
     * 初始化cookies
     *
     * @throws IOException
     */
    public static void initCookies() {
        cookies = login(Constant.FENGWO_USERNAME, Constant.FENGWO_PASSWORD);
    }

    /**
     * 初始化文件
     */
    public static void initPath() {
        try {
            Files.createDirectories(Paths.get(Constant.BASE_PATH, Constant.WEN_TI));
            Files.createDirectories(Paths.get(Constant.BASE_PATH, "account"));
        } catch (IOException e) {
            LOGGER.error("初始化工作空间失败");
        }

    }

    public static synchronized Date parse2Date(String time, SimpleDateFormat sdf) {
        Date parse;
        LOGGER.debug("parse time:{}", time);
        try {
            parse = sdf.parse(time);
        } catch (ParseException e) {
            return new Date();
        }
        return parse;
    }

    /**
     * 抓取问题页面
     *
     * @param question
     * @return 抓到的问题
     */
    public static Question fetchQuestion(Question question) {
        LOGGER.info("开始抓取问题:{}", question.getQid());
        try {
            String url = String.format(Constant.QUESTIONS, question.getQid());
            Document doc = Jsoup.connect(url).userAgent(Constant.USER_AGENT).referrer(REFERRER).timeout(Constant.TIMEOUT).get();
            Elements select = doc.select(".btn-loadMore");
            if (select.size() > 0) {
                Element answer = doc.select(".answer-list").first();
                answer.append(loadMoreAnswer(question.getQid(), Constant.ANSWER_NUM));
            }

            int youYong = countYouYong(doc);
            int tongWen = findNum(doc.select(".num-with").first().text(), NUMWITHPATTERN);
            int liuLan = findNum(doc.select(".visit").first().text(), NUMWITHPATTERN);
            int huiDa = findNum(doc.select("#_j_anum").first().text(), NUMWITHPATTERN);
            String content = doc.html();
            question.setTongWen(tongWen);
            question.setLiuLan(liuLan);
            question.setHuiDa(huiDa);
            question.setYouYong(youYong);
            question.setContent(content);
            question.setFengMi(findNum(doc.select(".reward-honey").text()));
            question.setAid(findNum(doc.select(".q-detail .person .avatar a").attr("href")));
            question.setTimes(parse2Date(doc.select(".q-content .time").text(), QUESTION_TIME));
            question.setTitleNum(doc.select(".q-title h1").text().length());
            question.setContentNum(doc.select(".q-info .q-desc").text().length());
            question.setComeFrom(doc.select(".q-content .user-bar .fr a").text());
            question.setStatus(Status.OK);
            LOGGER.info("抓取问题:{}", question);
        } catch (Exception e) {
            LOGGER.error("抓取问题:{},异常：{}", question.getQid(), e.getMessage());
            question.setStatus(handleException(e));
        }
        return question;
    }

    /**
     * 抓取问题页面
     *
     * @param question
     * @return 抓到的问题
     */
    public static Question fetchQuestionAID(Question question) {
        LOGGER.info("开始抓取问题:{}", question.getQid());
        try {
            String url = String.format(Constant.QUESTIONS, question.getQid());
            Document doc = Jsoup.connect(url).userAgent(Constant.USER_AGENT).referrer(REFERRER).timeout(Constant.TIMEOUT).get();
            question.setFengMi(findNum(doc.select(".reward-honey").text()));
            question.setAid(findNum(doc.select(".q-detail .person .avatar a").attr("href")));
            LOGGER.info("结束抓取问题:{}", question);
        } catch (Exception e) {
            LOGGER.error("抓取问题:{},异常：{}", question.getQid(), e.getMessage());
            question.setStatus(handleException(e));
        }
        return question;
    }

    public static List<Answer> parse2Answer(int qid, String content) {
        List<Answer> answers = new ArrayList<Answer>();
        Document parse;
        Answer answer;
        parse = Jsoup.parse(content);
        for (Element answer_item : parse.select(".answer-wrap .answer-item")) {
            answer = new Answer();
            answer.setQid(qid);
            String uid = answer_item.select(".person .avatar").attr("data-uid");
            answer.setAid(Integer.valueOf(StringUtil.isBlank(uid) ? "0" : uid));
            answer.setContentNum(answer_item.select(".answer-content ._j_answer_html").text().length());
            answer.setYouYong(Integer.valueOf(answer_item.select(".answer-action .zan .btn-zan span").text()));
            answer.setTimes(parse2Date(answer_item.select(".answer-content .user-bar .time").text(), ANSWER_TIME));
            answer.setZuiJia(!answer_item.select(".icon-best").isEmpty());
            answer.setZuiKuai(!answer_item.select(".icon-first").isEmpty());
            LOGGER.info("解析qid:{},answer:{}", qid, answer);
            answers.add(answer);
        }
        return answers;
    }


    /**
     * 抓取用户点拼页面
     *
     * @param qid 开始抓取问题Id
     * @return 抓到的问题
     */
    public static String fetchReview(int qid) {
        String content;
        try {
            LOGGER.info("抓取用户：{}点平", qid);
            String url = String.format(Constant.DIPING_URL, qid);
            Document doc = Jsoup.connect(url).userAgent(Constant.USER_AGENT).referrer(REFERRER).timeout(Constant.TIMEOUT).cookies(cookies).get();
            int reviews = Integer.valueOf(doc.select(".num-reviews a b").text());
            if (reviews > 20) {
                Element review = doc.select("#_j_poilist").first();
                review.append(loadMoreReview(qid, 20, cookies));
            }
            content = doc.html();
        } catch (Exception e) {
            LOGGER.error("[可忽略]抓取页面出错, 索引: {}, 异常:{}", qid, e.getMessage());
            throw new MFWException(e);
        }
        return content;
    }

    /**
     * 解析目标文件的review
     *
     * @param aid  用户Id
     * @param path 文件路径
     * @return 所有review结果
     */
    public static List<Review> parse2Reviews(int aid, String content) {

        List<Review> reviews = new ArrayList<Review>();
        Document parse;
        Review review;
        parse = Jsoup.parse(content);
        for (Element poi_item : parse.select(".poi-item")) {
            if (!poi_item.select(".from").isEmpty()) {
                LOGGER.info("来自于评论{}忽略",poi_item.select(".from").text());
                continue;
            }
            review = new Review();
            review.setAid(aid);
            Elements poi_detail = poi_item.select(".poi-detail .s-ding a.icon-ding");
            review.setName(poi_detail.attr("data-poiname"));
            review.setPid(Integer.valueOf(poi_detail.attr("data-poiid")));
            review.setRid(Integer.valueOf(poi_detail.attr("data-id")));
            review.setPingFen(poi_item.select(".rating").attr("data-star"));
            review.setTuiJian(poi_item.select(".rating .info .tip").text());
            String content_text = poi_item.select("._j_comment").text();

            review.setContent(content_text);
            review.setContentNum(content_text.length());
            review.setImgNum(poi_item.select(".poi-photo a").size());
            review.setTimes(parse2Date(poi_item.select(".poi-btns .time").text(), SDF));
            LOGGER.info("解析review:{}", review);
            reviews.add(review);
        }
        return reviews;
    }


    public static String loadMoreReview(int qid, int reviewId, Map<String, String> cookies) {
        String review;
        try {
            String url = String.format(Constant.DIPING_MORE, reviewId, qid);
            Connection.Response response = Jsoup.connect(url).userAgent(Constant.USER_AGENT).referrer(REFERRER).timeout(Constant.TIMEOUT).cookies(cookies).ignoreContentType(true).header("Accept", "application/json").execute();
            String content = response.body();
            Map map = gson.fromJson(content, Map.class);
            Boolean hasMore = (Boolean) ((Map) map.get("data")).get("hasmore");
            review = (String) ((Map) map.get("data")).get("html");
            if (hasMore) {
                return review + loadMoreReview(qid, reviewId + 20, cookies);
            }
        } catch (Exception e) {
            LOGGER.error("用户：{}加载更多{}点拼失败,{}", qid, reviewId, e.getMessage());
            throw new MFWException(e);
        }
        return review;
    }

    /**
     * 抓取用户游记页面
     *
     * @param qid 开始抓取用户ID
     * @return 抓到的问题
     */
    public static String fetchNote(int qid) {
        String content;
        try {
            LOGGER.info("抓取用户：{}游记", qid);
            String url = String.format(Constant.NOTE_URL, qid);
            Document doc = Jsoup.connect(url).userAgent(Constant.USER_AGENT).referrer(REFERRER).timeout(Constant.TIMEOUT).cookies(cookies).get();

            Element noteList = doc.select(".notes_list").first();
            doc.select(".m-pagination").remove();
            noteList.append(loadMoreNotes(qid, 2));
            content = doc.html();
        } catch (Exception e) {
            LOGGER.error("[可忽略]抓取页面出错, 索引: {},{}", qid, e.getMessage());
            throw new MFWException(e);
        }
        return content;
    }


    /**
     * 解析游记
     *
     * @param aid
     * @param path
     * @return
     */
    public static List<Note> parse2Notes(int aid, String content) {
        List<Note> notes = new ArrayList<Note>();
        Document parse;
        Note note;
        parse = Jsoup.parse(content);
        for (Element note_item : parse.select(".notes_list ul li")) {
            note = new Note();
            note.setAid(aid);
            if (!note_item.select(".MDing").isEmpty()) {
                note.setNid(Integer.valueOf(note_item.select(".MDing a").attr("data-iid")));
            }
            note.setTitle(note_item.select(".note_title .note_info h3 a").attr("title"));
            note.setTimes(parse2Date(note_item.select(".note_more .time").text(), SDF));
            LOGGER.info("解析note:{}", note);
            notes.add(note);
        }
        return notes;
    }

    /**
     * 抓取用户提问记录
     *
     * @param qid 开始抓取用户ID
     * @return 抓到的问题Id列表
     */
    public static Set<Integer> fetchMyQuestionsAnswer(String type, int qid) {
        Set<Integer> questionsAnswers = new HashSet<Integer>();
        try {
            LOGGER.info("抓取用户：{} {}记录", qid, type);
            String firstUrl = type.equals("question") ? String.format(Constant.MY_QUESTION_FIRST, qid) : String.format(Constant.MY_ANSWER_FIRST, qid);
            int pageSize = type.equals("question") ? 20 : 10;
            Document doc = Jsoup.connect(firstUrl).userAgent(Constant.USER_AGENT).referrer(REFERRER).timeout(Constant.TIMEOUT).cookies(cookies).get();
            for (Element element : doc.select(".ask_info h2 a")) {
                questionsAnswers.add(findNum(element.attr("href"), WENDA_ID));
            }
            String num = type.equals("question") ? doc.select(".gather_info ul.flt1 li:not(.gather_gold) a[href*=question] strong span").text() : doc.select(".gather_info ul.flt1 li:not(.gather_gold) a[href*=answer] strong span").text();

            if (Integer.valueOf(num) > pageSize) {
                String baseUrl = type.equals("question") ? Constant.MY_QUESTION_URL : Constant.MY_ANSWER_URL;
                for (int start = pageSize; ; start += pageSize) {
                    String url = String.format(baseUrl, qid, qid, start);
                    doc = Jsoup.connect(url).userAgent(Constant.USER_AGENT).referrer(REFERRER).timeout(Constant.TIMEOUT).cookies(cookies).ignoreContentType(true).get();

                    Map map = gson.fromJson(doc.text(), Map.class);
                    String html = (String) ((Map) map.get("payload")).get("list_html");
                    for (Element element : Jsoup.parse(html).select(".ask_info h2 a")) {
                        questionsAnswers.add(findNum(element.attr("href"), WENDA_ID));
                    }
                    String page = (String) ((Map) map.get("payload")).get("page_html");
                    if (StringUtil.isBlank(page)) {
                        break;
                    }
                }
            }

        } catch (Exception e) {
            LOGGER.error("抓取用户：{} {},错误{} ", qid, type, e.getMessage());
            throw new MFWException(e);
        }
        return questionsAnswers;
    }


    /**
     * 抓取用户游记更多页面
     *
     * @param uid     用户ID
     * @param page    分页数
     * @return
     */
    public static String loadMoreNotes(int uid, int page) {
        String note;
        try {
            Map<String, String> postData = new HashMap<String, String>();
            postData.put("sAction", "getArticle");
            postData.put("iPage", page + "");
            postData.put("iUid", uid + "");
            Document response = Jsoup.connect(Constant.NOTE_MORE).userAgent(Constant.USER_AGENT).referrer(REFERRER).timeout(Constant.TIMEOUT).cookies(cookies).ignoreContentType(true).header("Accept", "application/json").data(postData).post();

            Map map = gson.fromJson(response.text(), Map.class);
            note = (String) ((Map) map.get("payload")).get("html");
            if (note == null) {
                return note + loadMoreNotes(uid, page + 1);
            }
            Document doc = Jsoup.parse(note);
            note = doc.select(".notes_list").html();
            if (StringUtil.isBlank(note)) {
                return "";
            } else {
                return note + loadMoreNotes(uid, page + 1);
            }

        } catch (Exception e) {
            LOGGER.error("用户：{}加载更多{}点拼失败,{}", uid, page, e.getMessage());
            throw new MFWException(e);
        }
    }


    /**
     * 算出有用数
     *
     * @param doc 文档内容
     * @return 有用数
     */
    private static int countYouYong(Document doc) {
        int total = 0;
        for (Element element : doc.select(".btn-zan")) {
            total += findNum(element.text(), NUMWITHPATTERN);

        }
        return total;
    }

    /**
     * 加载更多答案
     *
     * @param qid   问题Id
     * @param index 答案起始Id
     * @return 答案列表
     */
    public static String loadMoreAnswer(int qid, int index) {
        String answer;
        try {
            Document doc = Jsoup.connect(String.format(Constant.ANSWER, qid, index)).referrer(REFERRER).userAgent(Constant.USER_AGENT).ignoreContentType(true).timeout(Constant.TIMEOUT).cookies(cookies).get();
            String content = doc.text();
            Map map = gson.fromJson(content, Map.class);
            answer = (String) ((Map) map.get("payload")).get("list_html");
            String page_html = (String) ((Map) map.get("payload")).get("page_html");
            if (page_html != null && !page_html.trim().isEmpty()) {
                answer = answer + loadMoreAnswer(qid, index + Constant.ANSWER_NUM);
            }
        } catch (Exception e) {
            LOGGER.error("抓取更多答案出错, qid: {},index:{},{}", qid, index, e.getMessage());
            throw new MFWException(e);
        }
        return answer;
    }

    public static Status handleException(Exception e) {
        if (e instanceof MFWException) {
            if (e.getCause() instanceof SocketTimeoutException) {
                return Status.TIMEOUT;
            }
            if (e.getCause() instanceof HttpStatusException) {
                return Status.NOT_FOUND;
            }
        }
        if (e instanceof SocketTimeoutException) {
            return Status.TIMEOUT;
        }
        if (e instanceof HttpStatusException) {
            return Status.NOT_FOUND;
        }
        return Status.ERROR;
    }


    /**
     * 查找字符串中数字. 例: "有用 (2)" -> 2
     *
     * @param li     查找字符串
     * @param patern
     * @return 返回数字
     */
    private static int findNum(String li, Pattern patern) {
        String num = "0";
        Matcher matcher = patern.matcher(li);
        if (matcher.find()) {
            num = matcher.group("num");
        }
        return Integer.valueOf(num);
    }

    public static int findNum(String str) {
        return findNum(str, PATERN);
    }


    /**
     * 保存字符串到文件
     *
     * @param path    文件路径
     * @param content 文件内容
     * @return
     */
    public static boolean str2File(String path, String content) {
        OutputStreamWriter os = null;
        boolean success = true;
        try {
            os = new OutputStreamWriter(new FileOutputStream(path), "UTF-8");
            os.write(content);
            os.flush();
        } catch (IOException e) {
            LOGGER.error("保存字符串{}到文件{}出错:{}", content, path, e.getMessage());
            success = false;
        } finally {
            if (os != null) {
                try {
                    os.close();
                } catch (IOException e) {
                }
            }
        }
        return success;
    }


    /**
     * 模拟登陆,并返回cookies
     * 1. 获取验证码code.
     * 2. 连接打码平台获取验证码
     * 3. 验证验证码是否正确
     * 4. post登陆
     *
     * @param userName 马蜂蜗用户名
     * @param password 马蜂蜗密码
     * @return 返回cookies
     */
    public static Map<String, String> login(String userName, String password) {
        Map<String, String> cookies = new HashMap<String, String>();
        for (int i = 0; i < 3; i++) {
            LOGGER.info("第{} 次尝试登陆", i);
            try {
                Connection.Response execute = Jsoup.connect(Constant.GET_CODE).ignoreContentType(true).timeout(Constant.TIMEOUT).execute();
                Files.write(Paths.get("maFengCode.jpg"), execute.bodyAsBytes(), StandardOpenOption.CREATE);
                Scanner scanner = new Scanner(System.in);
                LOGGER.info("请对照输入验证码（maFengCode+i）：");
                String code = scanner.nextLine();
                Document document = Jsoup.connect(Constant.VERIFY_CODE_URL + code).timeout(Constant.TIMEOUT).cookies(execute.cookies()).ignoreContentType(true).userAgent(Constant.USER_AGENT).get();
                if (code.equals(document.text())) {
                    document = Jsoup.connect(Constant.LOGIN).data("passport", userName).data("password", password)
                            .data("code", code).timeout(Constant.TIMEOUT).cookies(execute.cookies()).post();
                    if (document.select(".login-info #_j_head_user").size() > 0) {
                        cookies = execute.cookies();
                        break;
                    }
                }
            } catch (IOException e) {
                LOGGER.error("登陆出错:{}", i, e);
            }
        }
        if (cookies.size() == 0) {
            throw new RuntimeException("登陆失败,请稍后再试");
        }
        return cookies;
    }

    /**
     * 从一张问题html文件中抓取所有用户
     *
     * @param content 问题html
     * @return 所有用户列表
     */
    public static Set<Account> fetchAccounts(String content) {
        Set<Account> accounts = new HashSet<Account>();
        Account account;
        Document document = Jsoup.parse(content);
        for (Element element : document.select(".user-bar .name")) {
            account = new Account();
            account.setNickName(element.text());
            account.setUid(getUid(element.attr("href")));
            accounts.add(account);
            LOGGER.info("抓取用户：{}", account);
        }
        return accounts;
    }

    private static Integer getUid(String text) {
        Matcher matcher = UID_PATTERN.matcher(text);
        Integer uid = 0;
        if (matcher.find()) {
            uid = Integer.valueOf(matcher.group("uid"));
        }
        return uid;
    }


    public static void getAccount(Account account) {
        account.setComplete(true);
        account.setUid(account.getUid());
        try {
            Document document = Jsoup.connect(String.format(Constant.HOME_PAGE, account.getUid())).userAgent(Constant.USER_AGENT).cookies(cookies).timeout(Constant.TIMEOUT).get();

            account.setLevel(document.select(".MAvaLevel a").first().text());
            Elements sex = document.select(".MAvaName i");
            account.setSex(sex.size() == 0 ? Sex.MF : sex.hasClass("MGenderFemale") ? Sex.M : Sex.F);
            Elements home = document.select(".MAvaPlace");
            account.setHome(home.size() == 0 ? "" : home.attr("title"));
            account.setNickName(document.select(".MAvaName").text());
            account.setGuanZhu(Integer.valueOf(document.select(".MAvaNums a[href~=follow]").text()));
            account.setFenSi(Integer.valueOf(document.select(".MAvaNums a[href~=fans]").text()));
            account.setFengMi(Integer.valueOf(document.select(".MAvaNums a[href~=mall]").text()));
            account.setStatus(Status.OK);
            LOGGER.info("抓取用户{}", account);
        } catch (Exception e) {
            LOGGER.error("获取用户{}信息失败", account.getUid(), e);
            account.setStatus(handleException(e));
        }
    }
}
